#pragma once

#include <iostream>
#include <fstream>
#include <string>
#include <exception>
#include <tuple>
#include <cstring>
#include <cstdlib>
#include <cstdint>
#if defined (__linux__) || defined (__APPLE__)
#include <unistd.h>
#include "limits.h"
#elif defined (_WIN32)  || defined (_WIN64)
#include <windows.h>
#endif
#include <sys/stat.h>

namespace easyfs
{
    struct path_sepr;
    using string_type = std::string;
    using c_str_type = const char*;
    using int64_type = uint64_t;
    using time_type = time_t;
    using sepr_type = char;

    struct path_sepr
    {
        enum seprs
        {
            win = '\\',
            unix = '/',
        };

        static sepr_type sepr;

        static bool is_sepr(const sepr_type sepr) { return sepr == win || sepr == unix; }
    };

    sepr_type path_sepr::sepr =
#if defined (_WIN32)  || defined (_WIN64)
        path_sepr::win;
#elif defined (__linux__) || defined (__APPLE__)
        seprs::unix;
#endif

    class path;
    extern path absolute(const path&);
    extern path relative(const path&);

    class path
    {
    public:
        friend std::ostream& operator<<(std::ostream& os, const path& p);
        friend std::istream& operator>>(std::istream& is, path& p);

    public:
        path() : _p("") {}

        path(c_str_type p) : _p(p) {}

        path(const string_type& p) : _p(p) {}

        path(const path& p) 
        {
            if (&p != this) _p = p._p;
        }

        path& operator=(const path& p)
        {
            if (&p != this) _p = p._p;
            return *this;
        }

        path& assign(const string_type& s)
        {
            return *this = s;
        }

        path operator+(const path& p) const 
        {
            path tmp(*this);
            return tmp += p;
        }
        path& operator+=(const path& p)
        {
            if (path_sepr::is_sepr(_p.back()))
                _p += p._p;
            else
                _p += path_sepr::sepr + p._p;
            return *this;
        }
        path  operator/ (const path& p) const { return *this +  p; }
        path& operator/=(const path& p) { return *this += p; }

        path& append(const path& p) { return *this += p; }

        void clear() { _p.clear(); }

        size_t size() const { return _p.size(); }

        void change_sepr(const sepr_type sepr)
        {
            sepr_type old_sepr = 
                (sepr == path_sepr::unix ? path_sepr::win : path_sepr::unix);

            size_t pos = _p.find(old_sepr); 
            while (pos != string_type::npos)
            {
                _p[pos] = sepr;
                pos = _p.find(old_sepr); 
            }
        }

        void remove_filename()
        {
            size_t pos = _p.rfind(path_sepr::sepr);
            _p.erase(pos + 1);
        }

        void remove_top_directory()
        {
            size_t pos = _p.find(path_sepr::sepr);
            _p = _p.substr(pos + 1);
        }

        void replace_top_directory(const path& p)
        {
            remove_top_directory();
            *this = p / _p;
        }

        void replace_filename(const path& replace)
        {
            remove_filename();
            _p += replace._p;
        }

        void replace_extension(const path& replace)
        {
            size_t pos = _p.rfind('.');
            _p.erase(pos);
            _p += replace._p;
        }

        const char* c_str() const { return _p.c_str(); }

        string_type& string() { return _p; }
        const string_type& string() const { return _p; }

        path absolute_path() const { return absolute(_p); }
        path relative_path() const { return relative(_p); }

        // get the parent directory
        const path parent_path() const 
        {
            const auto& res = split(path_sepr::sepr);

            if (std::get<2>(res) == true)
                return std::get<0>(res);
            else
                return ".";
        }

        // get the filename and extension
        path filename() const 
        {
            const auto& res = split(path_sepr::sepr);

            if (std::get<2>(res) == true)
                return std::get<1>(res);
            else
                return *this;
        }

        // get the filename but no extension
        path stem() const 
        {
            return std::get<0>(filename().split('.'));
        }

        // get the extension
        path extension() const 
        {
            return '.' + std::get<1>(filename().split('.'));
        }

        bool operator==(const path& p) const { return _p == p._p; }
        bool operator!=(const path& p) const { return _p != p._p; }
        bool operator< (const path& p) const { return _p <  p._p; }
        bool operator<=(const path& p) const { return _p <= p._p; }
        bool operator> (const path& p) const { return _p >  p._p; }
        bool operator>=(const path& p) const { return _p >= p._p; }

    private:
        std::tuple<string_type, string_type, bool> split(const sepr_type sepr) const
        {
            return split(_p, sepr);
        }
        std::tuple<string_type, string_type, bool> split(const path& p, const sepr_type sepr) const
        {
            return split(p._p, sepr);
        }
        std::tuple<string_type, string_type, bool> split(const string_type& s, const sepr_type sepr) const
        {
            size_t pos = _p.rfind(sepr);

            if (pos == string_type::npos)
                return { "", "", false };
            else
                return { _p.substr(0, pos), _p.substr(pos + 1), true };
        }

    private:
        string_type _p;
    };

    std::ostream& operator<<(std::ostream& os, const path& p)
    {
        os << p._p;
        return os;
    }

    std::istream& operator>>(std::istream& is, path& p)
    {
        is >> p._p;
        return is;
    }

    path current_path()
    {
#if defined (_WIN32)  || defined (_WIN64)
        char tmp[MAX_PATH] = { 0 };
        if (GetCurrentDirectoryA(sizeof(tmp), tmp) == 0)
#else
        char tmp[PATH_MAX] = { 0 };
        if (getcwd(tmp, sizeof(tmp)) == nullptr)
#endif
            throw std::runtime_error("easyfs path error: relative_path");

        return tmp;
    }

    void current_path(const path& p)
    {
#if defined (_WIN32)  || defined (_WIN64)
        if (!SetCurrentDirectoryA(p.c_str()))
#else
        if (chdir(p.c_str()) != 0)
#endif
            throw std::runtime_error("easyfs current_path error");
    }

    path absolute(const path& rp)
    {
#if defined (__linux__) || defined (__APPLE__)
        char path[PATH_MAX] = { 0 };
        if (!realpath(rp.c_str(), path))
#elif defined (_WIN32)  || defined (_WIN64)
        char path[MAX_PATH] = { 0 };
        if (GetFullPathNameA(rp.c_str(), sizeof(path), path, nullptr) == 0)
#endif
            throw std::runtime_error("easyfs path error: absolute");
        return path;
    }

    path relative(const path& ap)
    {
        path cwd = current_path();

        size_t pos = ap.string().find(cwd.string());
        if (pos == string_type::npos)
            throw std::runtime_error("easyfs path error: relative");

        return ap.string().substr(pos + cwd.size()  + 1); // cwd string won't end with '/', so it needs to be +1.
    }
}
